GitにおけるHEADの移動を理解する
はじめに
Gitを使用する上で、HEADの概念を理解していることは非常に重要です。特に複数ブランチにまたがって開発をする場合、HEADの仕組みや動きを理解しておくことで、誤った操作やトラブルを未然に防ぐことができます。本記事ではHEADの仕組みを解説し、マージ操作を取り消す際の考え方を紹介します。
HEADとは
まず、Gitの用語としてheadとHEADの二種類があります。それぞれ、公式ドキュメントでは以下のように説明されています。
Git - user-manual Documentation
- head
A named reference to the commit at the tip of a branch. Heads are stored in a file in
$GIT_DIR/refs/heads/
directory, except when using packed refs. (See git-pack-refs[1].)
それぞれのブランチの先端にあるコミット(最後に行われたコミット)を指すための目印をheadといいます。例えば、master
ブランチの先端にあるコミットはrefs/heads/master
として管理されます。もしそのブランチで新たにコミットが行われた場合、そのコミットを指すように更新されます。
現在のリポジトリに存在するheadの一覧を参照するには、git show-ref
コマンドを使用します。このコマンドは、現在のリポジトリ内で各ブランチやタグがどのコミットを指しているかを管理します。
$ git show-ref
0d6e9cbcce255a7b8079ec8464cdfc2df610ccff refs/heads/branch-3
6af46e52b1f8ee0ee4787869a4e548ee01de3d82 refs/heads/master
ここで示されているコミットハッシュ(0d6e9cbcce255a7b8079ec8464cdfc2df610ccff
などの値のこと)が、各ブランチの先端のコミットを意味します。
例えば、下図のようにブランチが派生しているとします。branch-3
ブランチの先端のコミット、つまりEを指すものがrefs/heads/branch-3
、master
ブランチの先端のコミットGを指すものがrefs/heads/master
として管理されます。
D---E [branch-3]
/
A---B---C---F---G [master]
- HEAD
The current branch. In more detail: Your working tree is normally derived from the state of the tree referred to by HEAD. HEAD is a reference to one of the heads in your repository, except when using a detached HEAD, in which case it directly references an arbitrary commit.
現在のブランチを指す特別な目印をHEADといいます。現在のブランチのheadを指します。masterブランチで作業していればrefs/heads/master
がHEADであり、branch-3ブランチで作業していればrefs/heads/branch-3
がHEADとなります。
現在のブランチを確認するには、git branch
コマンドを使用します。
$ git branch
branch-3
* master
*
マークがついているブランチが現在のブランチです。
HEADが移動するタイミング
前述したとおり、HEADは現在のブランチのheadを指します。そして、ブランチのheadはそのブランチの先端のコミットが変わるたびに更新されます。
つまり、HEADは以下のタイミングで移動します。
- ブランチを切り替えたとき
ブランチをmaster
からbranch-3
へ切り替えると、HEADはmaster
ブランチのheadからbranch-3
ブランチのheadを新たに指し直します。
- 現在のブランチでheadが更新されたとき
headが更新されたときというのは、先端のコミットが変わったときです。つまり、新たにコミットが行われたとき、マージが成功したとき、コミットを取り消したときなどです。
HEADの移動履歴を確認する方法
HEADの移動履歴はgit reflog
コマンドで確認できます。
Git - git-reflog Documentation
$ git reflog
6af46e5 (HEAD -> master) HEAD@{0}: checkout: moving from branch-3 to master
7865309 (branch-3) HEAD@{1}: commit: branch-3 commit 2
96f2809 HEAD@{2}: commit: branch3 commit
0d6e9cb HEAD@{3}: checkout: moving from master to branch-3
6af46e5 (HEAD -> master) HEAD@{4}: checkout: moving from branch-3 to master
0d6e9cb HEAD@{5}: commit: branch3
6af46e5 (HEAD -> master) HEAD@{6}: checkout: moving from master to branch-3
上にいくほど新しく、下にいくほど古い履歴です。
現在のHEADは、(HEAD -> master)
と書かれている、6af46e5
のコミットを指し示しています。
commit:
と書かれている行は、新たにコミットを行いheadの指す先が変わったことによるHEADの移動です。checkout:
と書かれている行は、ブランチを切り替えたことによるHEADの移動です。
また、先頭に(branch-3)
や(HEAD -> master)
などブランチ名がついている行とついていない行があります。先頭にブランチ名が書かれているコミットは、各ブランチのheadが指す先のコミットです。下の図でいうと、EやGのコミットであるということを表しています。
D---E [branch-3]
/
A---B---C---F---G [master]
先頭にブランチ名が書かれていないコミットは、DやFなど、履歴としては存在しているが、headとして管理されていないコミットであることを表します。
つまり、先頭にブランチ名が書かれているのは各ブランチの最新コミット、書かれていないのは最新ではないコミットです。
この状態からmaster
ブランチで新しくコミットをし、再度git reflog
を実行します。今まで先頭にブランチ名が書かれていたコミットからブランチ名が消え、もうそのコミットは最新ではなくなったことがわかります。
$ git reflog
acfa83a (HEAD -> master) HEAD@{0}: commit: master new commit
6af46e5 HEAD@{1}: checkout: moving from branch-3 to master
7865309 (branch-3) HEAD@{2}: commit: branch-3 commit 2
96f2809 HEAD@{3}: commit: branch3 commit
0d6e9cb HEAD@{4}: checkout: moving from master to branch-3
6af46e5 HEAD@{5}: checkout: moving from branch-3 to master
0d6e9cb HEAD@{6}: commit: branch3
6af46e5 HEAD@{7}: checkout: moving from master to branch-3
HEADが移動したかどうかでマージの取り消し操作が異なる
誤ってpullやmergeをしてしまい、操作を取り消したい場合、上記のようなHEADの移動を理解していることが重要です。
pullやmergeを行い、コンフリクトせずに成功した場合
この場合、マージしたという内容のコミットが作成されます。これにより、HEADの指す先がそのコミットに変更されます。そのため、pullやmergeを取り消したい場合は、HEADが指す先を直前のコミットに戻す必要があります。
HEADが指す先を戻すために使用するコマンドはgit reset
です。記法はgit reset <mode> <commit>
です。
<mode>
にはsoft
、mixed
、hard
の3つのモードがあります。それぞれ、どこまでリセットするかで以下のような違いがあります。mixed
を指定した場合、ステージング状態は解除されますが、作業内容は保持されます。
ステージングエリアのリセット | 作業内容のリセット | |
---|---|---|
soft | されない | されない |
mixed(デフォルト) | される | されない |
hard | される | される |
<commit>
はいくつか指定方法があります。1つ前のコミットに戻す場合、最も簡単な記法はHEAD^
です。また、コミットハッシュや、HEAD@{1}
のように指定することもできます。
git reset --mixed HEAD^
リセットした後に再度git reflog
で確認します。
$ git reflog
6af46e5 (HEAD -> master) HEAD@{0}: reset: moving to HEAD^
acfa83a HEAD@{1}: commit: master new commit
6af46e5 (HEAD -> master) HEAD@{2}: checkout: moving from branch-3 to master
7865309 (branch-3) HEAD@{3}: commit: branch-3 commit 2
96f2809 HEAD@{4}: commit: branch3 commit
0d6e9cb HEAD@{5}: checkout: moving from master to branch-3
6af46e5 (HEAD -> master) HEAD@{6}: checkout: moving from branch-3 to master
0d6e9cb HEAD@{7}: commit: branch3
実際には履歴を削除したわけではなく、「リセットした」という新たな履歴が追加されています。そのため、リセットする前の状態に再度戻すこともできます。
pullやmergeを行い、コンフリクトした場合
コンフリクトした場合、マージは中断され、コミットは作成されません。つまり、HEADの指し示す先は変わっていません。この場合、単にマージを取り消すだけでpullやmergeする前の状態に戻ることができます。マージを取り消すには、次のコマンドを使用します。
git merge --abort
おわりに
本記事ではGitにおけるHEADの仕組みについて解説しました。
この記事がどなたかの役に立てれば幸いです。